﻿using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Text;
using System.Threading;
using System.Xml;
using log4net;
using Sherwood.Content.Contracts;
using Sherwood.Content.Tasks;
using IsolationLevel = System.Transactions.IsolationLevel;
using System.Linq;

namespace Sherwood.Content.UpdateServices
{
    public class XmlUpdateService<TRecord> : IXmlUpdateService where TRecord : class, IRecord
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(XmlUpdateService<>));

        /// <summary>
        /// This service receives a list of records that will be updated.
        /// For each record meta a <see cref="IMetaXmlUpdater"/> (according to configured handler) will be called
        /// and take responsibility for creating or updating that specific meta.
        /// </summary>
        /// <param name="xml">Xml following schema Sherwood.Content.Contracts.updateRecordsRequest</param>
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="InvalidOperationException"></exception>
        public Stream UpdateRecords(Stream xml)
        {
            Log.Debug("Entering method UpdateRecords");

            try
            {
                var msResult = new MemoryStream();
                using (var result = XmlWriter.Create(msResult, new XmlWriterSettings { OmitXmlDeclaration = true, Indent = false }))
                using (var dataSession = DependencyInjection.Resolve<IDataSessionProvider>().CreateScope())
                {
                    result.WriteStartElement("success");

                    var request = updateRecordsRequest.Load(new StreamReader(xml));

                    var client = DependencyInjection.Resolve<IClientRepository>()[request.client.id];
                    if (!request.VerifySignature(client.PublicKey))
                        throw new ArgumentException("Invalid signature");
                    var records = DependencyInjection.Resolve<IRecordRepository<TRecord>>();

                    if (Log.IsInfoEnabled)
                        Log.InfoFormat("Processing request from {0}. Records: {1}", client.Id,
                                       request.content.record.Count);

                    var updatedRecords = new List<TRecord>();
                    var deletedRecords = new List<Guid>();
                    foreach (var xmlRecord in request.content.record)
                    {
                        try
                        {
                            // if no clientId is specified, it is defaulted to be thesending client
                            Guid? deletedRecordId;
                            var record = GetRecord(client, xmlRecord, records, out deletedRecordId);
                            if (record != null)
                                updatedRecords.Add(record);
                            else if (deletedRecordId.HasValue)
                                deletedRecords.Add(deletedRecordId.Value);
                        }
                        catch (Exception ex)
                        {
                            var xmlReader = xmlRecord.Untyped.CreateReader();
                            var recordXml = string.Empty;
                            if (xmlReader.Read())
                                recordXml = xmlReader.ReadOuterXml();
                            Log.Error("Failed to update record " + recordXml, ex);

                            result.WriteStartElement("warning");
                            result.WriteElementString("exception", ex.ToString());

                            result.WriteStartElement("source");
                            result.WriteRaw(recordXml);
                            result.WriteEndElement();

                            result.WriteEndElement();
                        }
                    }

                    if (deletedRecords.Count > 0)
                        SearchDocumentUpdater<TRecord>.Delete(deletedRecords);
                    if (updatedRecords.Count > 0)
                        SearchDocumentUpdater<TRecord>.Update(updatedRecords, 200, true);

                    dataSession.Complete();

                    result.WriteEndElement();
                }

                msResult.Position = 0;
                return msResult;
            }
            catch (Exception exception)
            {
                Log.Error("Call failed", exception);

                var msResult = new MemoryStream();
                using (var result = XmlWriter.Create(msResult, new XmlWriterSettings { OmitXmlDeclaration = true, Indent = false }))
                    result.WriteElementString("exception", exception.ToString());
                msResult.Position = 0;
                return msResult;
            }
            finally
            {
                Log.Debug("Leaving method UpdateRecords");
            }
        }

        private static TRecord GetRecord(IClient client, record xmlRecord, IRecordRepository<TRecord> records, out Guid? deletedRecordId)
        {
            deletedRecordId = null;
            TRecord record;
            var updateMode = (UpdateMode)Enum.Parse(typeof(UpdateMode), xmlRecord.updateMode);
            var recordClientId = string.IsNullOrEmpty(xmlRecord.clientId)
                                     ? client.Id
                                     : xmlRecord.clientId;
            var allowCreate = updateMode == UpdateMode.Create || updateMode == UpdateMode.CreateOrUpdate;
            if (!string.IsNullOrEmpty(recordClientId) && !string.IsNullOrEmpty(xmlRecord.referenceId))
            {
                // we try to get the record, and if it does not exist we create it, given we have the right updateMode etc.
                record = records.GetByReferenceId(recordClientId, xmlRecord.referenceId);
                if (record == null)
                {
                    if (client.Id != recordClientId)
                    {
                        // we are not allowed to create for other clients,
                        // so depending on update mode we either throw an exception or continue to the next one
                        if (allowCreate)
                            throw new InvalidOperationException(
                                "Cannot create a record for another client. Use updateMode 'Update'.");
                        return null;
                    }
                    if (allowCreate)
                        record = records.Create(client, xmlRecord.referenceId, xmlRecord.title);
                }
            }
            else if (!string.IsNullOrEmpty(xmlRecord.id))
            {
                // since we need the referenceId to create a new record, we just always check that an updateMode with create is not used without it.
                if (allowCreate)
                    throw new InvalidOperationException(
                        string.Format("Cannot use update mode {0} when no referenceId is specified.", updateMode));

                record = records.GetById(Guid.Parse(xmlRecord.id));
                if (record == null)
                {
                    if (Log.IsDebugEnabled)
                        Log.Debug("Record " + xmlRecord.id + " not found");
                    if (updateMode == UpdateMode.Delete)
                        deletedRecordId = Guid.Parse(xmlRecord.id); // delete from search document (just in case it is still there)
                    return null;
                }
            }
            else
                throw new ArgumentException(
                    "No identifier specified for record. Either unique id or clientId + referenceId must be given.");

            if (record == null) return null;

            if (updateMode == UpdateMode.Delete)
            {
                deletedRecordId = record.Id;
                records.Delete(record);
                return null;
            }

            record.Description = xmlRecord.description;
            record.Title = xmlRecord.title;
            record.CultureInvariantReferenceId = xmlRecord.cultureInvariantReferenceId;
            record.CultureName = xmlRecord.culture;
            record.LanguageCode = xmlRecord.language;

            var changedMetas = new List<IMeta>();

            foreach (var xmlMeta in xmlRecord.meta)
            {
                // based on the configured handler we get a xml meta updater
                var meta = UpdateRecordMeta(client, record, xmlMeta);
                if (meta != null)
                {
                    // we store all created/updated metas to provide them to the search field updaters later in the method
                    changedMetas.Add(meta);
                }
            }
            return record;
        }

        private static IMeta UpdateRecordMeta(IClient client, IRecord record, record.metaLocalType xmlMeta)
        {
            var xmlUpdater = DependencyInjection.Resolve<IMetaXmlUpdater>(xmlMeta.handler);
            if (xmlUpdater != null)
            {
                try
                {
                    var updateMode = (UpdateMode)Enum.Parse(typeof(UpdateMode), xmlMeta.updateMode);
                    return xmlUpdater.FromXml(record, client, xmlMeta.usageKey,
                                              updateMode, xmlMeta.Untyped.CreateReader());
                }
                catch (Exception ex)
                {
                    var xmlReader = xmlMeta.Untyped.CreateReader();
                    var xml = string.Empty;
                    if (xmlReader.Read())
                        xml = xmlReader.ReadOuterXml();
                    Log.Error("Failed to update meta for record " + record.Id + ":\n" + xml, ex);
                }
            }
            return null;
        }

        private static readonly object RebuildingIndexLock = new object();

        public Stream RebuildSearchIndex(string client)
        {
            if (!Monitor.TryEnter(RebuildingIndexLock, 0))
                return new MemoryStream(Encoding.UTF8.GetBytes("<busy />"));
            Monitor.Exit(RebuildingIndexLock);
            ThreadPool.QueueUserWorkItem(OnRebuildSearchIndex, client);
            return new MemoryStream(Encoding.UTF8.GetBytes("<initiated />"));
        }

        private static void OnRebuildSearchIndex(object state)
        {
            if (!Monitor.TryEnter(RebuildingIndexLock, 0))
                return;
            var client = (string) state;
            Log.Info("Starting full search index rebuild" + (string.IsNullOrEmpty(client) ? string.Empty : " for client " + client));
            try
            {
                // TODO: Should be optimized to not load everything at once. Should still run updating in short transactions.
                // TODO: Segment the run, e.g. on id 2-3 letter start. Run in parallel?

                // TODO: Refactor data session handling to use a kind of AOP (unity interception or post sharp?));
                using (var dataSession = DependencyInjection.Resolve<IDataSessionProvider>().CreateSession())
                {
                    var repository = DependencyInjection.Resolve<IRecordRepository<TRecord>>();

                    Queue<TRecord> records;
                    using (var scope = dataSession.CreateScope(IsolationLevel.Snapshot))
                    {
                        var query = repository.All;
                        if (!string.IsNullOrEmpty(client))
                            query = query.Where(r => r.Client.Id == client);
                        records = new Queue<TRecord>(query.OrderBy(r => r.DateIndexed).ToList());

                        scope.Complete();
                    }

                    while (records.Count > 0)
                    {
                        using (var scope = dataSession.CreateScope(IsolationLevel.RepeatableRead))
                        {
                            var list = new List<TRecord>(100);
                            for (int i = 0; i < 100 && records.Count > 0; i++)
                                list.Add(records.Dequeue());
                            SearchDocumentUpdater<TRecord>.Update(list, 100, true);

                            scope.Complete();
                        }
                    }

                }
            }
            catch (Exception exception)
            {
                Log.Error("RebuildSearchIndex failed", exception);
            }
            finally
            {
                Log.Debug("Full search index rebuild finished");
                Monitor.Exit(RebuildingIndexLock);
            }
        }
    }
}